/*
 * Copyright 2011 myTDev.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.mytdev.swing.actions;

import java.awt.event.ActionEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.swing.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The actions manager build the actions for a specified object and then use the
 * Action annotation to link the actions with the object fields. Actions
 * properties are retreive through a resource bundle that can be specified. If
 * the resource bundle is not specified, the actions manager try to load the
 * resource bundle defined by the managed object class name.
 *
 * @author Yann D'Isanto
 */
public class ActionsManager {

    private static final Logger LOG = LoggerFactory.getLogger(ActionsManager.class);
    private static final String ACTION_RESOURCE_KEY_PREFIX = "Action.";
    private Object objectToManage;
    private Map<String, javax.swing.Action> actions = new HashMap<String, javax.swing.Action>();

    /**
     * Instanciates an ActionsManager for the specified object.
     * @param objectToManage the object to manage.
     */
    public ActionsManager(Object objectToManage) {
        this.objectToManage = objectToManage;
    }

    /**
     * Builds and links the actions for the managed object.
     */
    public void buildActions() {
        buildActions(null);
    }

    /**
     * Builds and links the actions for the managed object. Actions properties 
     * are loaded through the specified resource bundle.
     * @param resources the resource bundle.
     */
    public void buildActions(ResourceBundle resources) {
        actions = new HashMap<String, javax.swing.Action>();
        Class<?> objectClass = objectToManage.getClass();
        if (resources == null) {
            try {
                resources = ResourceBundle.getBundle(objectClass.getName());
            } catch (MissingResourceException ex) {
                LOG.debug("no resource bundle found for " + objectClass.getName(), ex);
            }
        }
        for (final Method method : objectClass.getMethods()) {
            final Action actionAnnotation = method.getAnnotation(Action.class);
            if (actionAnnotation != null) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length > 1) {
                    throw new IllegalArgumentException("too many parameters for action method: " + parameterTypes.length);
                } else if (parameterTypes.length > 0) {
                    if (!parameterTypes[0].isAssignableFrom(ActionEvent.class)) {
                        throw new IllegalArgumentException("invalid action method parameter: " + parameterTypes[0].getName());
                    }
                }

                javax.swing.Action action = resources != null
                        ? createResourceAction(resources, method, actionAnnotation)
                        : createAction(method, actionAnnotation);

                StringBuilder actionKeyBuilder = new StringBuilder(actionAnnotation.value());
                if (actionKeyBuilder.length() == 0) {
                    actionKeyBuilder.append(method.getName());
                }
                String actionKey = actionKeyBuilder.toString();
                actions.put(actionKey, action);
            }
        }
        for (Field field : objectClass.getDeclaredFields()) {
            Action actionAnnotation = field.getAnnotation(Action.class);
            if (actionAnnotation != null) {
                Class<?> fieldType = field.getType();
                if ((!AbstractButton.class.isAssignableFrom(fieldType))
                        && (!JTextField.class.isAssignableFrom(fieldType))
                        && (!JComboBox.class.isAssignableFrom(fieldType))) {
                    throw new IllegalArgumentException("field " + field.getName() + " can't handle actions");
                }
                javax.swing.Action action = getAction(actionAnnotation.value());
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                try {
                    Method method = fieldType.getMethod("setAction", javax.swing.Action.class);
                    method.invoke(field.get(objectToManage), action);
                } catch (IllegalAccessException ex) {
                    LOG.error(ex.getLocalizedMessage(), ex);
                    // TODO OR NOT TODO: throw an exception
                } catch (InvocationTargetException ex) {
                    LOG.error(ex.getLocalizedMessage(), ex);
                    // TODO OR NOT TODO: throw an exception
                } catch (NoSuchMethodException ex) {
                    // Never appends
                    LOG.error(ex.getLocalizedMessage(), ex);
                }
                field.setAccessible(accessible);
            }
        }
    }

    /**
     * Returns the action associated with the specified key.
     * @param actionKey an action key.
     * @return the action associated with the specified key. Returns null if no 
     * action is associated with the specified key.
     */
    public javax.swing.Action getAction(String actionKey) {
        return actions.get(actionKey);
    }

    /**
     * Returns an ActionMap containing the actions of this manager.
     * @return an ActionMap containing the actions of this manager. 
     */
    public ActionMap getActionMap() {
        ActionMap actionMap = new ActionMap();
        for (Map.Entry<String, javax.swing.Action> entry : actions.entrySet()) {
            actionMap.put(entry.getKey(), entry.getValue());
        }
        return actionMap;
    }

    /**
     * Creates and returns an action performing a call to the specified method.
     * @param method the method to invoke on actionPerformed.
     * @param actionAnnotation the action annotation.
     * @return an Action instance.
     */
    private javax.swing.Action createAction(final Method method, final Action actionAnnotation) {
        String actionName = actionAnnotation.value();
        if (actionName.isEmpty()) {
            actionName = method.getName();
        }
        return new AbstractAction(actionName) {

            @Override
            public void actionPerformed(final ActionEvent actionEvent) {
                Runnable actionRunnable = new Runnable() {

                    @Override
                    public void run() {
                        try {
                            if (method.getParameterTypes().length == 0) {
                                method.invoke(objectToManage);
                            } else {
                                method.invoke(objectToManage, actionEvent);
                            }
                        } catch (InvocationTargetException ex) {
                            Throwable cause = ex.getTargetException();
                            LOG.error(cause.getMessage(), cause);
                        } catch (Exception ex) {
                            LOG.error(ex.getLocalizedMessage(), ex);
                        }
                    }
                };
                switch (actionAnnotation.thread()) {
                    case BACKGROUND:
                        new Thread(actionRunnable).start();
                        break;
                    case EDT:
                        SwingUtilities.invokeLater(actionRunnable);
                        break;
                }
            }
        };
    }

    /**
     * Creates and returns an action performing a call to the specified method.
     * Action properties are loaded from the specified resource bundle.
     * @param resources the resource bundle.
     * @param method the method to invoke on actionPerformed.
     * @param actionAnnotation the action annotation.
     * @return an Action instance.
     */
    private javax.swing.Action createResourceAction(ResourceBundle resources, final Method method, final Action actionAnnotation) {
        StringBuilder actionKeyBuilder = new StringBuilder(actionAnnotation.value());
        if (actionKeyBuilder.length() == 0) {
            actionKeyBuilder.append(method.getName());
        }
        actionKeyBuilder.insert(0, ACTION_RESOURCE_KEY_PREFIX);
        String actionResourceKey = actionKeyBuilder.toString();
        final javax.swing.Action action = createAction(method, actionAnnotation);
        return new ResourceAbstractAction(resources, actionResourceKey) {

            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                action.actionPerformed(actionEvent);
            }
        };
    }
}
